A Philosophy of Software Design
https://gyazo.com/c1fad82e1bce74b701c78655c515f4ed
TL;DR
ソフトウェアには様々な複雑性がある
それぞれに対して戦略的なアプローチをする必要がある
そのためには以下が重要
shallow moduleではなくdeep moduleが良いmoduleである
いいコメントやいい名前づけは複雑度を減らす
この本の中では、複雑度にまつわるものに名前をつけて複雑性への理解とその戦略的なアプローチへの理解を学ぶ
Introduction to Complexity
Complexity is anything related to the structure of a sowtfare system that makes it hard to understand and modify the system
あるタスクを実装しようとするときに生じる変更箇所の数
あるタスクを実装しようとしたときに生じる技術調査や実装するために変更が必要な箇所の学習量
これが大きいとそれだけ多くのことを学習する必要がある
またこれが高いとそれだけ考慮漏れによるバグや不具合が生じる可能性が高い
Sometimes an approach that requires more lines of code is actually simpler, because it reduces cognitive load.
あるタスクを実装しようとしたときに何をすべきかがわからない状態
この現象が最悪で、実装するにあたって知りたいことがなんであるかも知らない状態なので学習するのも苦労する
Cause of complexitiy
依存が多いとそれだけ影響範囲が大きくなる
import や 見えてるinterfaceの数 や そのクラスが使用されている箇所など
ある情報が自明でないときに曖昧さがある
例えば変数名やメソッド名からコメント不足やドキュメント不足など
誰にでも伝わるような設計があればドキュメントは少なくて済むのでこれも設計によって改善の余地がある
Strategic vs Tactical Programming
Working code isn't enough
タスクを終わらせることに特化した方法
コードは機能するだけでは十分ではないという考えに基づく方法
複雑度の低いコードを書くことに投資することで、短いスパンではスピードが落ちるが、長い目でみると費用対効果が高くなると考えられる
10 ~ 20 % の全体の時間を複雑度の低いコードを書くための調査や実装などにかけるべきである
よいコードベースは技術的な優位性やマーケットでの成長速度を産むのでスタートアップだからといってそこに投資をしないというのはあまりおすすめしない
Modules
In an ideal world, each moduke would be copletely independent of the others.
Unfortunately, this ideal is not achievable.
The goal of modular design is to minimize the dependencies between modules.
モジュールと一概にいってもクラスだけではなく、interface と 実装があればそれは全てモジュールとして扱うことが可能。例えばHTTPリクエストなどもmoduleといえる
最高のモジュールは、実装よりもシンプルなinterfaceがあるモジュールのこと
他のmoduleに課す複雑度が下がる
interfaceを変更しないような変更のときには、他のmoduleに一切の影響を与えない
formal vs informal information
メソッドのシグネチャや引数の数、型など字面からわかる明示されている情報
副作用やメソッドの呼び出し順序やメソッドの制約などのドキュメンテーションなどのみを通じて知ることができる情報
Abstractions
An abstraction is a simplified view of an entity, which omits unimportant details
In modular programming, each module provides an abstraction in form of its interface.
Interfaceは、moduleの機能をシンプルな形式で提供している
Abstraction、Interfaceから unimportant details が排除されることは重要で unimportant というのに注意しないと失敗することがある
unimportant な情報をabstractionに含めてしまう
unimportant では、ない重要な情報を排除してしまう
obscurityが大きくなり、開発者はabstractionから欲しい情報が引き出せなくなってしまう
Deep Modules
shallow moduleは複雑度が増してる赤信号なので気をつけるとよい
Classitis
「クラスは小さくあるべき」という風潮のこと
code:java
FileInputStream fileStream = new FileInputStream(fileName);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileStream);
ObjectInputStream objectInputStream = new ObjectInputStream(bufferedStream);
↑のように無駄に小さく責務がクラスで分かれているのは赤信号、Bufferがいるかどうかユーザーが選べる方が柔軟かもしれないが、シンプルなユースケースは必ず1つデフォルトとして提供すべきである。上記の例でいえば new ObjectInputStream(); でFileからBufferつきで読めてもおかしくない。
Information Hiding (and Leakage)
Information Hiding
再三話に出てるように、moduleは機能を持つべきである。つまりは、知識を持つべき。
これによって複雑度が下がる
機能が中に隠れるのでInterfaceはよりシンプルになる
機能が中に隠れることでmodule間の依存は少なくなり、システム全体の柔軟度が高くなる
Privateなフィールドなどは、知識を隠す手助けはしてくれるが知識を実際に隠してるわけではない
Information Leakage
hidingとは逆でmoduleの知識が外に表出してしまってる場合
染み出し方はいくつかあり
interfaceに染み出してしまう
暗黙的な依存や制約を持ってしまってる場合
インスタンスが初期化されてる前提とか
メソッドの呼び出し順とか
fileの読み書きなどで同じパーザーのロジックを使ってる場合には、複数の場所で前提が暗黙的に存在しておりleakしてると言える
General Purpose Modules
moduleの設計をするときによく対峙するシチュエーションは、general-purpose にするか special-purpose にするか
おすすめは、 somewhat general-purpose
機能は現在のニーズを満たすような具体的な機能でよくて、ただしinterfaceは一般性を持って設計するというもの
例えばテキストエディタを設計する場合には、
code:Java
// special-purpose
void backspace(Cursor cursor);
void delete(Cursor cursor);
// somewhat general-purpose
void insert(Position position, String newText);
void delete(Position start, Position end);
機能は、テキスト削除などに特化してるがinterfaceは一般的になっている。
Different Layer, Different Abstraction
Layerが違うということは、責任も違うということ
しかしLayerだけを作るというバッドデザインがたくさんある
Interfaceから実装が容易に想像できるようなものは、うまくabstractionしてるとはいえず多くの場合にはshallow moduleになる
Pass-through methods
code:Java
public class TextDocument ... {
private TextArea textArea;
private TextCodumentListener listener;
...
public Character getLastTypedCharacter(){
return textArea.getLastTypedCharacter();
}
public int getCursorOffset() {
return textArea.getCursorOffset();
}
public void insertString(String textToInsert, int offset) {
textArea.insertString(textToInsert, offset);
}
public void willInsertString(String stringToInsert, int offset) {
if(listener != null){
listener.willInsertString(this, stringToInsert, offset);
}
}
}
Layerを通ってるはずなのに何もせずに次のLayerにそのまま渡してしまうという実装パターン
メソッドが増えても複雑さが増すだけなので意味がない
Decorators
あるクラスに対して機能を拡張するためのクラスのことを指す
以下の方法で回避できないかを考える
元のクラスに実装を追加する
クラスではなく、処理をしているところにロジックを追加する
完全に独立した新しいクラスを作る方がよくないかを考える
Pass-through variables
code:Java
main(argc, argv); // argcからcertを取ってm1に渡す
m1(..., cert, ...); // m1では特にcertは使わない
m2(..., cert, ...); // m2では特にcertは使わない
m3(..., cert, ...) {
...
openSocket(cert, ...);
...
}
共有してるオブジェクトに詰めるようにする
グローバル変数にする
Contextオブジェクトにする
どの方法もその変数への依存を増やすのでシステム全体の複雑度をあげてしまう
筆者的には、Contextオブジェクトをimmutableにする方法がもっとも有効と考えてる
Better Together Or Better Apart
ある2つのコードを同じ場所におくか、離れたところに置くかという問題
基本的には、コンポーネントの数とコンポーネントの大きさはトレードオフになっている
いくつか指針が挙げられる
Bring together if information is shared
知識をシェアしているもの同士は同じ場所におくのがよい
例えばあるフォーマットのデータをパースする処理などが一箇所になるようにする
Bring together if it will simiplify the interface
Bring together to eliminate duplication
Separate general-purpose and special-purpose code
一般性のあるモジュールの中に具体的なユースケースでしか使わない処理を混ぜてはいけない
メソッドを分割したり、統合したりする際にはある機能の単位で独立して分けることが重要
Each method should do one thing and do it completely
他のメソッドの処理を理解しないといけないなどがあってはいけない
Exception
例外はソフトウェアを複雑にする大きな要因のひとつ
エラーの種類が多すぎる、またそれらをハンドリングするのが難しい
プログラミング言語的に綺麗にかける言語がない
状態が中途半端になったりする
例外のハンドリングの方法はおもに3つ
例外が起こっても続ける
例外を投げて呼び出しもとに通知する
これは中途半端なstateを生み出すので大体の場合においてはうまく行かないし複雑
さらに悪いことにあまり起こらない例外を書くと滅多に実行されないコードを書くことになるので将来的な複雑度に寄与する
code that hasn't been executed dosen't work
必要な例外を必要なだけ書くことが重要
例外を減らすことは複雑度を下げる点で重要だが、必要な例外まで減らすと柔軟性を下げるので注意が必要
Define errors out of existence
例外にならないようにAPIを定義する
例えば、flushのような機能で
データがあるときにデータを空にする : データがない場合には例外
空になることを保証する : データがなくても結果は保証できてるので例外を投げない
Mask exceptions
エラーを検知したあとに内部でそれをハンドリングして再送などを行うことで外側には知られないようにする
Exception aggregation
メソッドを呼び出すところですぐにハンドリングするのではなく、さらにトップレベルで吸収するようにする
code:Java
try {
if(...) {
handleUrl1(...);
} else if(...) {
handleUrl2(...);
} else {
handleUrl3(...);
}
} catch (NoSuchParameter e) {
// do something
}
↑ だと handleUrl1 handleUrl2 handleUrl3 でそれぞれ例外を処理することも可能だけどトップレベルでやる
Just Crash
以下のようなときには、諦めてアプリをクラッシュさせる
ほとんどその例外が起こらない
例外が起こった結果ハンドリングすることがない (out of memory がいい例)
Code Comments
コメントは、コード単体では表現できないコードを書いた人の思想を反映させたり、補足するものである
よく言われる why not を書くという話に繋がる考え
コメントの書き方
コメントはコードから明らかではないことを書く
convention (何を書くコメントかフォーマットはどうかなどの規約) を決める
Interface
Data Structure member
Implementation Comment
Cross-module comment
コードに書かれていることをコメントで繰り返さない
練習として例えば、異なる単語を使って説明してみる
コードと同じ抽象レベルでコメントを書かない
コードよりも lower level か higher level でコメントを書く
lower level : コードに具体性を追加する (implementation comment によく使う)
higher level : コードに理由を追加する (interface commentによく使う)
Higher Levelでどうやってそのメソッドを使うかを書く
引数が何を表すか、引数やメソッドの動作の制限など
Lower Levelで何がしたいかの思想のヒントを書く
コードを読む人は、何をしているかの直感的理解があるとコードは簡単に読めるので直感的に理解するための情報をコメントによって与える
Choosing Names for variables, methods ...
いい名前は2つの要素を持つ
precision
具体的かつ正確でないといけない
一般性のある名前は勘違いや複数人間での意味のぶれを招く
consistency
曖昧さを防ぐという意味でも同じ名前を同じ目的のために一貫性を持つということは重要である
以下を守るとよい名前づけができる
同じ目的のためには同じ言葉を使う
同じ目的ではないものには使わない
挙動がにぶれが出ないように目的を具体化させる
あくまでも指針であり、直感的かどうか読みやすいかどうかは読み手 (チームの自分以外のn-1人) が決める
Using Comments as a part of design process
コメントを書くという工程をコードを書くプロセスに組み込むことで設計をする助けにする
筆者は以下のプロセスでコードを書く
1. クラスのinterface commentを書く
2. 重要なpublicメソッドのinterfaceコメントとシグネチャを書く
3. 自分の設計やデザインがしっくり来るかを考えて方針を決める
4. クラスの重要な変数などに対してもコメントを書く
5. メソッドの処理の中身を書いて、implementation commentを必要に応じて書く
6. 5の過程で追加で必要になるメソッドなどもあるのでその度にinterface comment を書き必要に応じてimplementation commentを書く
感想
ソフトウェアの複雑性について考察している本
内容自体は誰でも一度は実際にもしくは何かの本で触れたことのある内容ばかりだがそれに対して良い名前をつけたり分解、考察をしている点がとても素晴らしい本
共通言語にしてチームの中での思想を合わせるにはとてもいい本